import type {
  ControlPosition,
  IControl,
  Layer,
  StyleSpecification,
} from 'mapbox-gl';

export declare interface MapboxLanguageOptions {
  /** List of supported languages */
  supportedLanguages?: string[];
  /** Custom style transformation to apply */
  languageTransform?: (
    style: StyleSpecification,
    language: string,
  ) => StyleSpecification;
  /**
   * RegExp to match if a text-field is a language field
   * @default /^name_/
   */
  languageField?: RegExp;
  /** Given a language choose the field in the vector tiles */
  getLanguageField?: (language: string) => string;
  /** Name of the source that contains the different languages. */
  languageSource?: string;
  /** Name of the default language to initialize style after loading. */
  defaultLanguage?: string;
  /** Name of the layers that should be excluded from translation. */
  excludedLayerIds?: string[];
}

export class MapboxLanguage implements IControl {
  private options: MapboxLanguageOptions;

  constructor(options: MapboxLanguageOptions) {
    this.options = options;
    this.options.languageField = options.languageField || /^name_/;
    this.options.getLanguageField =
      options.getLanguageField ||
      function nameField(language) {
        return language === 'mul' ? 'name' : `name_${language}`;
      };
    this.options.languageSource = options.languageSource || undefined;
    this.options.excludedLayerIds = options.excludedLayerIds || [];
    this.options.supportedLanguages = options.supportedLanguages || [
      'ar',
      'de',
      'en',
      'es',
      'fr',
      'it',
      'ja',
      'ko',
      'mul',
      'pt',
      'ru',
      'vi',
      'zh-Hans',
      'zh-Hant',
      // dz support
      'zh',
      'latin',
      'nonlatin',
    ];
  }

  setLanguageField(languageField: RegExp) {
    this.options.languageField = languageField;
  }

  setGetLanguageField(getLanguageField: (language: string) => string) {
    this.options.getLanguageField = getLanguageField;
  }

  isTokenField = /^\{name/;
  map: mapboxgl.Map | undefined;
  container: HTMLElement | undefined;

  private _current = '';
  get current() {
    return this._current;
  }
  set current(value) {
    this._current = value;
  }

  isFlatExpressionField(isLangField: RegExp, property: unknown) {
    const isGetExpression = Array.isArray(property) && property[0] === 'get';
    if (isGetExpression && this.isTokenField.test(property[1])) {
      console.warn(
        'This plugin no longer supports the use of token syntax (e.g. {name}). Please use a get expression. See https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/ for more details.',
      );
    }
    return isGetExpression && isLangField.test(property[1]);
  }

  adaptNestedExpressionField(
    isLangField: RegExp,
    property: unknown,
    languageFieldName: string,
  ) {
    if (Array.isArray(property)) {
      for (let i = 1; i < property.length; i++) {
        if (Array.isArray(property[i])) {
          if (this.isFlatExpressionField(isLangField, property[i])) {
            property[i][1] = languageFieldName;
          }
          this.adaptNestedExpressionField(
            isLangField,
            property[i],
            languageFieldName,
          );
        }
      }
    }
  }

  adaptPropertyLanguage(
    isLangField: RegExp,
    property: Array<unknown>,
    languageFieldName: string,
  ) {
    let ret = property;
    if (this.isFlatExpressionField(isLangField, property)) {
      ret[1] = languageFieldName;
    }
    this.adaptNestedExpressionField(isLangField, ret, languageFieldName);
    // handle special case of bare ['get', 'name'] expression by wrapping it in a coalesce statement
    if (ret[0] === 'get' && ret[1] === 'name') {
      const defaultProp = ret.slice();
      const adaptedProp = ['get', languageFieldName];
      ret = ['coalesce', adaptedProp, defaultProp];
    }
    return ret;
  }

  changeLayerTextProperty(
    isLangField: RegExp,
    layer: Layer,
    languageFieldName: string,
    excludedLayerIds: string[],
  ) {
    const txtField = (layer.layout as { [key: string]: unknown })?.[
      'text-field'
    ];
    if (txtField && excludedLayerIds.indexOf(layer.id) === -1) {
      return Object.assign({}, layer, {
        layout: Object.assign({}, layer.layout, {
          'text-field': this.adaptPropertyLanguage(
            isLangField,
            txtField as Array<unknown>,
            languageFieldName,
          ),
        }),
      });
    }
    return layer;
  }

  findStreetsSource(style: StyleSpecification) {
    const sources = Object.keys(style.sources);
    // const sources = Object.keys(style.sources).filter((sourceName) => {
    //   const source = style.sources[sourceName];
    //   // 由于 SourceSpecification 和 GeoJSONSourceSpecification 类型上不存在 url 属性，需要进行类型断言
    //   const url = (source as { url?: string }).url;
    //   // the source URL can reference the source version or the style version
    //   // this check and the error forces users to migrate to styles using source version 8
    //   return (
    //     (url && url.indexOf('mapbox.mapbox-streets-v8') > -1) ||
    //     /mapbox-streets-v[1-9][1-9]/.test(url || '')
    //   );
    // });
    if (!sources.length)
      throw new Error(
        'If using MapboxLanguage with a Mapbox style, the style must be based on vector tile version 8, e.g. "streets-v11"',
      );
    return sources[0];
  }

  setLanguage(style: StyleSpecification, language: string) {
    if (
      this.options?.supportedLanguages &&
      this.options?.supportedLanguages.indexOf(language) < 0
    )
      throw new Error(`Language ${language} is not supported`);
    const streetsSource =
      this.options.languageSource || this.findStreetsSource(style);
    if (!streetsSource) return style;

    this.current = language;

    const field = this.options.getLanguageField?.(language);
    const isLangField = this.options.languageField;
    const excludedLayerIds = this.options.excludedLayerIds;
    const changedLayers = style.layers.map((layer) => {
      if (
        layer.source === streetsSource &&
        isLangField &&
        field &&
        excludedLayerIds
      )
        return this.changeLayerTextProperty(
          isLangField,
          layer,
          field,
          excludedLayerIds,
        );
      return layer;
    });

    const languageStyle = Object.assign({}, style, {
      layers: changedLayers,
    });

    return this.options.languageTransform
      ? this.options.languageTransform(languageStyle, language)
      : languageStyle;
  }

  // TODO 这里初始化可以，但平时切主题图层时似乎有点浪费性能？
  _initialStyleUpdate() {
    const style = this.map?.getStyle();
    if (style && this.options.supportedLanguages) {
      const language =
        this.current ||
        this.options.defaultLanguage ||
        this.browserLanguage(this.options.supportedLanguages);
      language && this.map?.setStyle(this.setLanguage(style, language));
    }
  }

  browserLanguage(supportedLanguages: string[]) {
    const language = navigator.languages
      ? navigator.languages[0]
      : navigator.language;
    const parts = language.split('-');
    let languageCode = language;
    if (parts.length > 1) {
      languageCode = parts[0];
    }
    if (supportedLanguages.indexOf(languageCode) > -1) {
      return languageCode;
    }
    return null;
  }

  onAdd = (map: mapboxgl.Map) => {
    this.map = map;
    this.map.on('style.load', this._initialStyleUpdate.bind(this));
    this.container = document.createElement('div');
    return this.container;
  };

  onRemove = (_map: mapboxgl.Map) => {
    this.map?.off('style.load', this._initialStyleUpdate);
    this.map = undefined;
  };

  getDefaultPosition?: (() => ControlPosition) | undefined;

  _setLanguage?: ((language?: string | string[]) => void) | undefined;
}
